package phase2.eval;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileWriter;
import java.io.IOException;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.Scanner;

/**
 * An Evaluator, given a properly formatted input text file, walks the AST,
 * generates the results of each node, and propagate them. Statements are visited
 * in order, and expressions within statements and other expressions are evaluated
 * within the visit to their parent node. Statements supported include: print, replace,
 * and recursivereplace (assignments are implicit in the tree, as variables are
 * substituted with nodes representing whatever is on the right-hand side of the
 * assignment). Expressions supported include: find, length, union, inters, diff, and
 * maxfreqstring. Leaf nodes are either REGEXs or ASCII-STRs.
 * 
 * @author Devin Wang
 * @version Dec 8, 2012
 */
public class Evaluator {
	private Map<String, String> nodemap;
	private Map<String, String> varmap;

	/**
	 * Create an Evaluator that can be reused for new AST files.
	 * @throws FileNotFoundException
	 * @throws FileFormatException
	 * @throws IOException
	 */
	public Evaluator() throws FileNotFoundException, FileFormatException,IOException{
		nodemap = new HashMap<String, String>();
		varmap = new HashMap<String, String>();
	}
	
	/**
	 * Walk the AST, evaluating all statements
	 * @param filein the input txt file to walk
	 * @throws FileNotFoundException
	 * @throws FileFormatException
	 * @throws IOException
	 */
	public void walkAST(String filein) throws FileNotFoundException,FileFormatException,IOException{
		nodemap.clear();
		varmap.clear();
	
		Scanner scanner = new Scanner(new File(filein));
		while(scanner.hasNext()){
			String line = scanner.nextLine();
			if(line.isEmpty())
				continue;
			else{
				int space = line.indexOf(" ");
				if(space == -1)
					throw new FileFormatException("Not enough parameters in line: "+line);
				String nodeID = line.substring(0,space);
				String content = line.substring(space+1);
				if(nodeID != null && !nodeID.isEmpty() && content != null && !content.isEmpty())
					nodemap.put(nodeID,content);
			}
		}
		String[] parts = nodemap.get("0").split("statements");
		String[] statements = parts[1].split(" ");
		//Handle lines in the order that node 0 declares
		for(int i = 1; i < statements.length; i++){
			evalStatement(statements[i]);
		}
	}
	
	/**
	 * Only evaluate statements. Ignore expressions, as they are handled within statement evaluations
	 * and other expression evaluations.
	 * @param id the line to handle (if statement)
	 * @throws FileNotFoundException
	 * @throws FileFormatException
	 * @throws IOException
	 */
	private void evalStatement(String id) throws FileNotFoundException,FileFormatException,IOException{
		String statement = nodemap.get(id);
		String[] parts = statement.split(" ");

		if(parts.length > 1){ // not a leaf node
			String keyword = parts[0];
			if(keyword.equals("assign") || keyword.equals("length") || keyword.equals("maxfreqstring")){
				if(parts.length == 3){
					String varname = nodemap.get(parts[1]);
					if(parts[2].split(" ").length == 1)
						varmap.put(varname,nodemap.get(parts[2]));
					else
						varmap.put(varname,parts[2]);
				}
				else if(keyword.equals("assign"))
					throw new FileFormatException("Improper number of parameters for assign statement: " + statement);
				else if(keyword.equals("length"))
					throw new FileFormatException("Improper number of parameters for length statement: " + statement);
				else throw new FileFormatException("Improper number of parameters for maxfreqstring statement: " + statement);
			}
			else if(keyword.equals("print")){
				if(parts.length >= 2){
					List<String> explist = new ArrayList<String>();
					for(int x = 1; x < parts.length; x++){
						explist.add(parts[x]); // add expression to list to print
					}
					print(explist); // print list of expressions
				}
				else throw new FileFormatException("No parameters given for print statement: " + statement);
			}
			else if(keyword.equals("replace")){
				if(parts.length == 4)
					replace(nodemap.get(parts[1]),nodemap.get(parts[2]),nodemap.get(parts[3]));
				else throw new FileFormatException("Improper number of parameters for replace statement: " + statement);
			}
			else if(keyword.equals("recursivereplace")){
				if(parts.length == 4)
					recursiveReplace(nodemap.get(parts[1]),nodemap.get(parts[2]),nodemap.get(parts[3]));
				else throw new FileFormatException("Improper number of parameters for recursivereplace statement: " + statement);
			}
			else{
				// not a statement, do nothing
			}
		}
		else{
			// leaf node, do nothing
		}
	}
	
	/**
	 * Print the list of expressions. Length expressions printed as integers with a new line,
	 * string-match list expressions printed as tuples with a new line after each.
	 * @param explist
	 * @throws FileNotFoundException
	 * @throws FileFormatException
	 */
	private void print(List<String> explist) throws FileNotFoundException,FileFormatException{
		for(String nodeID : explist){
			String exp = nodemap.get(nodeID);
			String[] parts = exp.split(" ");
			if(parts.length == 1){ // given a variable name, either a match-list, length, or maxfreqstring var
				String[] contents = varmap.get(exp).split(" ");
				if(contents[0].equals("length")){ // length variable
					if(contents.length == 3) System.out.println(getLength(contents[1]));
				}
				else{ // match-list or maxfreqstring (technically also match-list) variable
					List<String>matchlist = evalExp(exp);
					for(String tuple : matchlist){
						System.out.println(tuple);
					}
				}
			}
			else{
				if(parts[0].equals("length")){
					System.out.println(getLength(parts[1]));
				}
				else{
					List<String> matchlist = evalExp(exp); // get the string-match list value of the expression
					for(String tuple : matchlist){
						System.out.println(tuple);
					}
				}
			}
		}
	}
	
	/**
	 * Replace all instances of a regex in an input file with a new string, and write
	 * the modified copy to an output file.
	 * @param regex the regex to be replaced
	 * @param str the new string to replace the regex
	 * @param filenames the input and output filenames
	 * @throws FileNotFoundException
	 * @throws IOException
	 */
	private void replace(String regex, String str, String filenames) throws FileNotFoundException,FileFormatException,IOException{
		regex = regex.substring(1,regex.length()-1);
		str = str.substring(1,str.length()-1);
		String names[] = filenames.split(" ");
		if(names.length == 3){
			String filename1 = nodemap.get(names[1]).substring(1,nodemap.get(names[1]).length()-1);
			String filename2 = nodemap.get(names[2]).substring(1,nodemap.get(names[2]).length()-1);
			Scanner scanner = new Scanner(new File(filename1));
			FileWriter writer = new FileWriter(filename2);
			while(scanner.hasNext()){
				String line = scanner.nextLine();
				String line2 = Pattern.compile(regex).matcher(line).replaceAll(str);
				writer.write(line2);
				writer.write("\n");
			}
			writer.close();
		}
		else throw new FileFormatException();
	}
	
	/**
	 * Recursively replace all instances of a regex in an input file with a new string,
	 * until no more modifications can be made, and write the final modified copy to an
	 * output file.
	 * @param regex the regex to be replaced
	 * @param str the new string to replace the regex
	 * @param filenames the input and output filenames
	 * @throws FileNotFoundException
	 * @throws IOException
	 */
	private void recursiveReplace(String regex, String str, String filenames) throws FileNotFoundException,FileFormatException,IOException{
		regex = regex.substring(1,regex.length()-1);
		str = str.substring(1,str.length()-1);
		String names[] = filenames.split(" ");
		if(names.length == 3){
			String filename1 = nodemap.get(names[1]).substring(1,nodemap.get(names[1]).length()-1);
			String filename2 = nodemap.get(names[2]).substring(1,nodemap.get(names[2]).length()-1);
			Scanner scanner = new Scanner(new File(filename1));
			FileWriter writer = new FileWriter(filename2);
			while(scanner.hasNext()){
				String line = scanner.nextLine();
				boolean changed = true;
				while(changed){
					String line2 = Pattern.compile(regex).matcher(line).replaceAll(str);
					if(line.equals(line2))
						changed = false;
					else
						line = line2;
				}
				writer.write(line);
				writer.write("\n");
			}
			writer.close();
		}
		else throw new FileFormatException();
	}
	
	/**
	 * Get the string-match list value of an expression. Called by print, #,
	 * union, inters, -, and maxfreqstring. Calls find, union, inters, -, and
	 * maxfreqstring.
	 * @param exp the expression to evaluate
	 * @return the string-match list value of the expression
	 * @throws FileNotFoundException
	 * @throws FileFormatException
	 */
	private List<String> evalExp(String exp) throws FileNotFoundException,FileFormatException{
		List<String> matchlist = new ArrayList<String>();
		String[] parts = exp.split(" ");
		if(parts.length == 1) matchlist = evalExp(varmap.get(parts[0]));
		else{
			if(parts[0].equals("find")){
				if(parts.length == 3){
					matchlist = find(nodemap.get(parts[1]),nodemap.get(parts[2]));
				}
				else
					throw new FileFormatException("Improper number of parameters for find expression: " + exp);
			}
			else if(parts[0].equals("union")){
				if(parts.length == 3){
					matchlist = union(parts[1],parts[2]);
				}
				else
					throw new FileFormatException("Improper number of parameters for union expression: " + exp);
			}
			else if(parts[0].equals("inters")){
				if(parts.length == 3){
					matchlist = inters(parts[1],parts[2]);
				}
				else
					throw new FileFormatException("Improper number of parameters for inters expression: " + exp);
			}
			else if(parts[0].equals("diff")){
				if(parts.length == 3){
					matchlist = diff(parts[1],parts[2]);
				}
				else
					throw new FileFormatException("Improper number of parameters for diff expression: " + exp);
			}
			else if(parts[0].equals("maxfreqstring")){
				if(parts.length == 3){
					matchlist = maxfreqstring(parts[1]);
				}
				else
					throw new FileFormatException("Improper number of parameters for maxfreqstring expression: " + exp);
			}
			else
				throw new FileFormatException("Unknown statement or expression: " + exp);
		}
		return matchlist;
	}

	/**
	 * Compile a string-match list from all instances of a regex in a source file.
	 * String-match entries are formatted as follows:
	 * matched-string <file-name, line, start-index, end-index>
	 * @param regex the regex to search for
	 * @param filename the file to search in
	 * @return a string-match list of tuples for all matches
	 * @throws FileNotFoundException
	 */
	private List<String> find(String regex, String filename) throws FileNotFoundException{
		regex = regex.substring(1,regex.length()-1);
		filename = filename.substring(1,filename.length()-1);
		List<String> matchlist = new ArrayList<String>();
		Scanner scanner = new Scanner(new File(filename));
		Pattern pattern = Pattern.compile(regex);
		int lineCount = 0;
		while(scanner.hasNext()){
			String line = scanner.nextLine();
			Matcher matcher = pattern.matcher(line);
			while(matcher.find()){
				String match = matcher.group() + " <\"" + filename + "\", " + lineCount + ", " + matcher.start() + ", " + matcher.end() + ">";
				if(!matchlist.contains(match)){
					matchlist.add(match);
				}
			}
			lineCount++;
		}
		return matchlist;
	}
	
	/**
	 * Get the length of a string-match list evaluated from an expression.
	 * @param matchlistnode the id of the expression node
	 * @return the number of elements in the list
	 * @throws FileNotFoundException
	 * @throws FileFormatException
	 */
	private int getLength(String matchlistnode) throws FileNotFoundException,FileFormatException{
		String exp= varmap.get(nodemap.get(matchlistnode));
		if(exp.split(" ").length == 1)
			return evalExp(varmap.get(exp)).size();
		else
			return evalExp(exp).size();
	}
	
	/**
	 * Get the union of two string-match lists.
	 * @param node1 the id of the expression node for the first list
	 * @param node2 the id of the expression node for the second list
	 * @return a new string-match list containing matches occurring in at least one of the two input nodes
	 * @throws FileNotFoundException
	 * @throws FileFormatException
	 */
	private List<String> union (String node1, String node2) throws FileNotFoundException,FileFormatException{
		List<String> matchlist1 = new ArrayList<String>();
		List<String> matchlist2 = new ArrayList<String>();
		List<String> newList = new ArrayList<String>();
		String exp1 = nodemap.get(node1);
		String exp2 = nodemap.get(node2);
		if(exp1.split(" ").length == 1){
			matchlist1 = evalExp(varmap.get(exp1));
		}
		else{
			matchlist1 = evalExp(exp1);
		}
		if(exp2.split(" ").length == 1){
			matchlist2 = evalExp(varmap.get(exp2));
		}
		else{
			matchlist2 = evalExp(exp2);
		}
		for(String s : matchlist1){
			if(!newList.contains(s)){
				newList.add(s);
			}
		}
		for(String s : matchlist2){
			if(!newList.contains(s)){
				newList.add(s);
			}
		}
		return newList;
	}
	
	/**
	 * Get the intersection of two string-match lists.
	 * @param node1 the id of the expression node for the first list
	 * @param node2 the id of the expression node for the second list
	 * @return a new string-match list containing matches occurring in both nodes
	 * @throws FileNotFoundException
	 * @throws FileFormatException
	 */
	private List<String> inters(String node1, String node2) throws FileNotFoundException,FileFormatException{
		List<String> matchlist1 = new ArrayList<String>();
		List<String> matchlist2 = new ArrayList<String>();
		List<String> newList = new ArrayList<String>();
		String exp1 = nodemap.get(node1);
		String exp2 = nodemap.get(node2);
		if(exp1.split(" ").length == 1){
			matchlist1 = evalExp(varmap.get(exp1));
		}
		else{
			matchlist1 = evalExp(exp1);
		}
		if(exp2.split(" ").length == 1){
			matchlist2 = evalExp(varmap.get(exp2));
		}
		else{
			matchlist2 = evalExp(exp2);
		}
		for(String s : matchlist1){
			if(matchlist2.contains(s) && !newList.contains(s)){
				newList.add(s);
			}
		}
		return newList;
	}
	
	/**
	 * Get the difference of one string-match list from the another string-match list.
	 * @param node1 the id of the expression node for the first list
	 * @param node2 the id of the expression node for the second list
	 * @return a new string-match list containing matches occurring in the first node and not in the second
	 * @throws FileNotFoundException
	 * @throws FileFormatException
	 */
	private List<String> diff(String node1, String node2) throws FileNotFoundException,FileFormatException{
		List<String> matchlist1 = new ArrayList<String>();
		List<String> matchlist2 = new ArrayList<String>();
		List<String> newList = new ArrayList<String>();
		String exp1 = nodemap.get(node1);
		String exp2 = nodemap.get(node2);
		if(exp1.split(" ").length == 1){
			matchlist1 = evalExp(varmap.get(exp1));
		}
		else{
			matchlist1 = evalExp(exp1);
		}
		if(exp2.split(" ").length == 1){
			matchlist2 = evalExp(varmap.get(exp2));
		}
		else{
			matchlist2 = evalExp(exp2);
		}
		for(String s : matchlist1){
			if(!matchlist2.contains(s) && !newList.contains(s)){
				newList.add(s);
			}
		}
		return newList;
	}
	
	/**
	 * Get the mode string in a string-match list
	 * @param matchlistnode the id of the expression node for the list
	 * @return a list of the string(s) that occur(s) the most in the list. Multiple strings possible
	 * if two or more strings occur at the mode number of instances.
	 * @throws FileNotFoundException
	 * @throws FileFormatException
	 */
	private List<String> maxfreqstring(String matchlistnode) throws FileNotFoundException,FileFormatException{
		List<String> matchlist = new ArrayList<String>();
		List<String> maxList = new ArrayList<String>();
		String exp = nodemap.get(matchlistnode);
		if(exp.split(" ").length == 1)
			matchlist = evalExp(varmap.get(exp));
		else
			matchlist = evalExp(exp);
		Map<String, Integer> countMap = new HashMap<String, Integer>();
		for(String match : matchlist){
			String key = match.split(" ")[0];
			if(!countMap.containsKey(key))
				countMap.put(key,0);
			else
				countMap.put(key, countMap.get(key)+1);
		}
		int mode = 0;
		for(int i : countMap.values()){
			if (i > mode)
				mode = i;
		}
		for(String key : countMap.keySet()){
			if(countMap.get(key) == mode)
				maxList.add(key);
		}
		return maxList;
	}
	
	/**
	 * FileFormatException thrown when the input txt file is improperly formatted.
	 * @author Devin Wang
	 *
	 */
	public static class FileFormatException extends Exception {
		private static final long serialVersionUID = 8448704966112475242L;

		public FileFormatException() {
		}

		public FileFormatException(String message, Throwable cause,
				boolean enableSuppression, boolean writableStackTrace) {
			super(message, cause, enableSuppression, writableStackTrace);
		}

		public FileFormatException(String message, Throwable cause) {
			super(message, cause);
		}

		public FileFormatException(String message) {
			super(message);
		}

		public FileFormatException(Throwable cause) {
			super(cause);
		}
	}
}
